package com.nhim.cms.admin.client.customwidget.ui;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.google.gwt.activity.shared.Activity;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.TextCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.requestfactory.shared.EntityProxyChange;
import com.google.gwt.requestfactory.shared.EntityProxyId;
import com.google.gwt.requestfactory.shared.Receiver;
import com.google.gwt.requestfactory.ui.client.EntityProxyKeyProvider;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiFactory;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.cellview.client.CellTable;
import com.google.gwt.user.cellview.client.Column;
import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.user.cellview.client.SimplePager;
import com.google.gwt.user.cellview.client.SimplePager.TextLocation;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.NoSelectionModel;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.RangeChangeEvent;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.nhim.cms.admin.client.GetValue;
import com.nhim.cms.admin.client.PhaseAnimation;
import com.nhim.cms.admin.client.SortableHeader;
import com.nhim.cms.admin.client.SpacerColumn;
import com.nhim.cms.admin.client.place.CustomWidgetListPlace;
import com.nhim.cms.admin.client.style.Styles;
import com.nhim.cms.admin.shared.CustomWidgetProxy;
import com.nhim.cms.admin.shared.NhimCMSAdminRequestFactory;

public class CustomWidgetList extends Composite implements
		EntityProxyChange.Handler<CustomWidgetProxy>, Activity {

	interface Binder extends UiBinder<Widget, CustomWidgetList> {
	}

	

	/**
	 * The resources applied to the table.
	 */
	interface TableResources extends CellTable.Resources {
		@Source({ CellTable.Style.DEFAULT_CSS,
				"../../style/cellTable.css" })
		TableStyle cellTableStyle();
	}

	/**
	 * The styles applied to the table.
	 */
	interface TableStyle extends CellTable.Style {

	}

	/**
	 * A text box that displays default text.
	 */
	private static class DefaultTextBox extends TextBox {

		/**
		 * The text color used when the box is disabled and empty.
		 */
		private static final String TEXTBOX_DISABLED_COLOR = "#aaaaaa";

		private final String defaultText;

		public DefaultTextBox(final String defaultText) {
			this.defaultText = defaultText;
			resetDefaultText();

			// Add focus and blur handlers.
			addFocusHandler(new FocusHandler() {
				public void onFocus(FocusEvent event) {
					getElement().getStyle().clearColor();
					if (defaultText.equals(getText())) {
						setText("");
					}
				}
			});
			addBlurHandler(new BlurHandler() {
				public void onBlur(BlurEvent event) {
					if ("".equals(getText())) {
						resetDefaultText();
					}
				}
			});
		}

		public String getDefaultText() {
			return defaultText;
		}

		/**
		 * Reset the text box to the default text.
		 */
		public void resetDefaultText() {
			setText(defaultText);
			getElement().getStyle().setColor(TEXTBOX_DISABLED_COLOR);
		}
	}

	/**
	 * A cell used to highlight search text.
	 */
	private class HighlightCell extends AbstractCell<String> {

		private static final String replaceString = "<span style='color:red;font-weight:bold;'>$1</span>";

		@Override
		public void render(Context context, String value, SafeHtmlBuilder sb) {
			if (value != null) {
				if (searchRegExp != null) {
					// The search regex has already been html-escaped
					value = searchRegExp.replace(
							SafeHtmlUtils.htmlEscape(value), replaceString);
					sb.append(SafeHtmlUtils.fromTrustedString(value));
				} else {
					sb.appendEscaped(value);
				}
			}
		}
	}

	private static final ProvidesKey<CustomWidgetProxy> keyProvider = new EntityProxyKeyProvider<CustomWidgetProxy>();
	/**
	 * The auto refresh interval in milliseconds.
	 */
	private static final int REFRESH_INTERVAL = 5000;

	private static Binder uiBinder = GWT.create(Binder.class);


	public static String getBreadcrumb(String menuItem) {
		return "All Custom Widgets";
	}


	@UiField
	SimplePager pager;
	@UiField
	Image searchButton;
	@UiField 
	Button btnInstall;

	@UiField(provided = true)
	DefaultTextBox searchBox;

	/**
	 * The main table. We provide this in the constructor before calling
	 * {@link UiBinder#createAndBindUi(Object)} because the pager depends on it.
	 */
	@UiField(provided = true)
	CellTable<CustomWidgetProxy> table;

	private final List<SortableHeader> allHeaders = new ArrayList<SortableHeader>();

	/**
	 * Indicates that the report count is stale.
	 */
	private boolean isCountStale = true;

	/**
	 * The field to sort by.
	 */
	private String orderBy = "name";

	/**
	 * The set of Report keys that we have seen. When a new key is added, we
	 * compare it to the list of known keys to determine if it is new.
	 */
	private Set<Object> knownReportKeys = null;

	/**
	 * Keep track of the last receiver so that we know if a response is stale.
	 */
	private Receiver<List<CustomWidgetProxy>> lastDataReceiver;

	/**
	 * Keep track of the last receiver so that we know if a response is stale.
	 */
	private Receiver<Integer> lastDataSizeReceiver;

	/**
	 * The {@link Timer} used to periodically refresh the table.
	 */
	private final Timer refreshTimer = new Timer() {
		@Override
		public void run() {
			isCountStale = true;
			requestCustomWidgets(true);
		}
	};

	/**
	 * The columns to request with each report.
	 */
	private final String[] customWidgetColumns = new String[] { "name",
			"description", "className" };

	/**
	 * The factory used to send requests.
	 */
	private final NhimCMSAdminRequestFactory requestFactory;

	/**
	 * The string that the user searched for.
	 */
	private RegExp searchRegExp;

	/**
	 * The starts with search string.
	 */
	private String startsWithSearch;

	private boolean running;

	public CustomWidgetList(NhimCMSAdminRequestFactory requestFactory) {
		this.requestFactory = requestFactory;

		// Initialize the widget.
		createTable();
		table.addRangeChangeHandler(new RangeChangeEvent.Handler() {
			public void onRangeChange(RangeChangeEvent event) {
				requestCustomWidgets(false);
			}
		});
		searchBox = new DefaultTextBox("search");
		initWidget(uiBinder.createAndBindUi(this));

		// Listen for key events from the text boxes.
		searchBox.addKeyUpHandler(new KeyUpHandler() {
			public void onKeyUp(KeyUpEvent event) {
				// Search on enter.
				if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
					search();
					return;
				}

				// Highlight as the user types.
				String text = SafeHtmlUtils.htmlEscape(searchBox.getText());
				if (text.length() > 0) {
					searchRegExp = RegExp.compile("(" + text + ")", "ig");
				} else {
					searchRegExp = null;
				}
				table.redraw();
			}
		});
		searchButton.addClickHandler(new ClickHandler() {
			public void onClick(ClickEvent event) {
				search();
			}
		});
	}

	public String mayStop() {
		return null;
	}

	public void onCancel() {
		onStop();
	}

	public void onProxyChange(EntityProxyChange<CustomWidgetProxy> event) {
		EntityProxyId<CustomWidgetProxy> changedId = event.getProxyId();
		List<CustomWidgetProxy> records = table.getVisibleItems();
		int i = 0;
		for (CustomWidgetProxy record : records) {
			if (record != null && changedId.equals(record.stableId())) {
				List<CustomWidgetProxy> changedList = new ArrayList<CustomWidgetProxy>();
				changedList.add(record);
				table.setRowData(i + table.getPageStart(), changedList);
			}
			i++;
		}
	}

	public void onStop() {
		running = false;
		refreshTimer.cancel();
	}


	public void start(AcceptsOneWidget panel, EventBus eventBus) {
		running = true;
		doUpdateForPlace();
		EntityProxyChange.registerForProxyType(eventBus, CustomWidgetProxy.class, this);
		requestCustomWidgets(false);
		panel.setWidget(this);
	}

	/**
	 * In this application, called by {@link ExpensesActivityMapper} each time a
	 * ReportListPlace is posted. In a more typical set up, this would be a
	 * constructor argument to a one shot activity, perhaps managing a shared
	 * widget view instance.
	 */
	public void updateForPlace(final CustomWidgetListPlace place) {
		if (running) {
			doUpdateForPlace();
		}
	}

	@UiFactory
	SimplePager createPager() {
		SimplePager p = new SimplePager(TextLocation.RIGHT);
		p.setDisplay(table);
		p.setRangeLimited(true);
		return p;
	}

	/**
	 * Add a sortable column to the table.
	 * 
	 * @param <C>
	 *            the data type for the column
	 * @param text
	 *            the header text
	 * @param cell
	 *            the cell used to render the column
	 * @param getter
	 *            the getter to retrieve the value for the column
	 * @param property
	 *            the property to sort by
	 * @return the column
	 */
	private <C> Column<CustomWidgetProxy, C> addColumn(final String text,
			final Cell<C> cell,
			final GetValue<CustomWidgetProxy, C> getter,
			final String property) {
		final Column<CustomWidgetProxy, C> column = new Column<CustomWidgetProxy, C>(
				cell) {
			@Override
			public C getValue(CustomWidgetProxy object) {
				return getter.getValue(object);
			}
		};
		final SortableHeader header = new SortableHeader(text);
		allHeaders.add(header);

		// Sort created by default.
		if ("Name".equals(property)) {
			header.setSorted(true);
			header.setReverseSort(true);
			orderBy = "Name" + " DESC";
		}

		header.setUpdater(new ValueUpdater<String>() {
			public void update(String value) {
				header.setSorted(true);
				header.toggleReverseSort();

				for (SortableHeader otherHeader : allHeaders) {
					if (otherHeader != header) {
						otherHeader.setSorted(false);
						otherHeader.setReverseSort(true);
					}
				}
				table.redrawHeaders();

				// Request sorted rows.
				orderBy = property;
				if (header.getReverseSort()) {
					orderBy += " DESC";
				}
				searchBox.resetDefaultText();
				searchRegExp = null;

				// Go to the first page of the newly-sorted results
				pager.firstPage();
				requestCustomWidgets(false);
			}
		});
		table.addColumn(column, header);
		return column;
	}

	/**
	 * Create the {@link CellTable}.
	 */
	private void createTable() {
		CellTable.Resources resources = GWT.create(TableResources.class);
		table = new CellTable<CustomWidgetProxy>(100, resources);
		table.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
		Styles.Common common = Styles.common();
		table.addColumnStyleName(0, common.spacerColumn());
		table.addColumnStyleName(1, common.customWidgetNameColumn());
		table.addColumnStyleName(2, common.customWidgetDescriptionColumn());
		table.addColumnStyleName(3, common.customWidgetClassNameColumn());
		table.addColumnStyleName(7, common.spacerColumn());

		// Add a selection model.
		final NoSelectionModel<CustomWidgetProxy> selectionModel = new NoSelectionModel<CustomWidgetProxy>();
		table.setSelectionModel(selectionModel);
		selectionModel
				.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
					public void onSelectionChange(SelectionChangeEvent event) {
						CustomWidgetProxy selectedCustomWidget = selectionModel
								.getLastSelectedObject();
						CustomWidgetInstance instance = new CustomWidgetInstance(requestFactory, selectedCustomWidget);
						instance.setGlassEnabled(true);
						instance.center();
					}
				});

		// Spacer column.
		table.addColumn(new SpacerColumn<CustomWidgetProxy>());

		// Name column.
		addColumn("Name", new HighlightCell(),
				new GetValue<CustomWidgetProxy, String>() {
					public String getValue(CustomWidgetProxy object) {
						return object.getName();
					}
				}, "name");

		// Description column.
		addColumn("Description", new TextCell(),
				new GetValue<CustomWidgetProxy, String>() {
					public String getValue(CustomWidgetProxy object) {
						return object.getDescription();
					}
				}, "description");
		
		// Input Type column.
		addColumn("Class Name", new TextCell(),
				new GetValue<CustomWidgetProxy, String>() {
					public String getValue(CustomWidgetProxy object) {
						return object.getClassName();
					}
				}, "className");

		
		table.addColumn(new SpacerColumn<CustomWidgetProxy>());
	}

	private void doUpdateForPlace() {
		isCountStale = true;
		searchBox.resetDefaultText();
		startsWithSearch = null;
		searchRegExp = null;
		pager.setPageStart(0);
		requestCustomWidgets(false);
	}

	/**
	 * Send a request for CustomWidget in the current range.
	 * 
	 * @param isPolling
	 *            true if this request is caused by polling
	 */
	private void requestCustomWidgets(boolean isPolling) {
		// Cancel the refresh timer.
		refreshTimer.cancel();

		// Early exit if we don't have a request factory to request from.
		if (requestFactory == null) {
			return;
		}
		// Clear the known keys.
		if (!isPolling) {
			knownReportKeys = null;
		}

		// Get the parameters.
		String startsWith = startsWithSearch;
		if (startsWith == null || searchBox.getDefaultText().equals(startsWith)) {
			startsWith = "";
		}

		// If a search string is specified, the results will not be sorted.
		if (startsWith.length() > 0) {
			for (SortableHeader header : allHeaders) {
				header.setSorted(false);
				header.setReverseSort(false);
			}
			table.redrawHeaders();
		}

		// Request the total data size.
		if (isCountStale) {
			isCountStale = false;
			if (!isPolling) {
				pager.startLoading();
			}
			lastDataSizeReceiver = new Receiver<Integer>() {
				@Override
				public void onSuccess(Integer response) {
					if (this == lastDataSizeReceiver) {
						int count = response.intValue();
						// Treat count == 1000 as inexact due to AppEngine
						// limitation
						table.setRowCount(count, count != 1000);
					}
				}
			};
			requestFactory.customWidgetRequest().countCustomWidgetBySearch(startsWith)
					.fire(lastDataSizeReceiver);
		}
		// Request reports in the current range.
		lastDataReceiver = new Receiver<List<CustomWidgetProxy>>() {
			@Override
			public void onSuccess(List<CustomWidgetProxy> newValues) {
				
				if (this == lastDataReceiver) {
					int size = newValues.size();
					if (size < table.getPageSize()) {
						// Now we know the exact data size
						table.setRowCount(table.getPageStart() + size, true);
					}
					if (size > 0) {
						table.setRowData(table.getPageStart(), newValues);
					}

					// Add the new keys to the known keys.
					boolean isInitialData = knownReportKeys == null;
					if (knownReportKeys == null) {
						knownReportKeys = new HashSet<Object>();
					}
					for (CustomWidgetProxy value : newValues) {
						Object key = keyProvider.getKey(value);
						if (!isInitialData && !knownReportKeys.contains(key)) {
							new PhaseAnimation.CellTablePhaseAnimation<CustomWidgetProxy>(
									table, value, keyProvider).run();
						}
						knownReportKeys.add(key);
					}
				}
				refreshTimer.schedule(REFRESH_INTERVAL);
			}
		};

		requestFactory
				.customWidgetRequest()
				.findCustomWidgetEntriesBySearch(startsWith, orderBy)
				.with(customWidgetColumns).fire(lastDataReceiver);

	}

	/**
	 * Search based on the search box text.
	 */
	private void search() {
		isCountStale = true;
		startsWithSearch = searchBox.getText();
		requestCustomWidgets(false);
	}
	public HasClickHandlers GetCreateNewLink()
	{
		return this.btnInstall;
	}
}
